all files / src/ gregorian-date.ts

87.63% Statements 85/97
76.27% Branches 45/59
92.31% Functions 12/13
87.1% Lines 81/93
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 1781× 1× 1× 1×             1×   1× 1×   1× 1×   1× 1×   1×   1×   1× 1× 9525×     776060× 776059× 776059×   776059×     1× 162446× 162446× 162446×   162446× 162446×   162446× 162446×   162446× 162446×   162446× 162446×       162446× 162336×     162446×   162446× 162446× 162446× 25966× 136480× 33331×   103149×     162446× 162446×   162446×           1×               1× 227711× 1501× 1501× 1501×     1× 7502×     1× 166444×     1× 318885×             1× 492837× 216252× 216252×     492837× 492837× 492837× 492837× 492837×   492837×     1× 492837× 492837×                                       1× 167945× 167945×                     1× 776059× 776059× 60100×   715959×     776059×       776059×   1×  
import { isLeapYear, parseDateString, JulianDays } from "./common";
import { HistoricalDate } from './historical-date';
import { InvalidDateException } from './invalid-date-exception';
import { JulianDate } from "./julian-date";
 
// Licensed under the MIT license:
// http://opensource.org/licenses/MIT
// Copyright (c) 2016, fitnr <fitnr@fakeisthenewreal>
// based on https://github.com/fitnr/convertdate/blob/5a1d1323c34670c5e179900d8770db59c009e679/convertdate/gregorian.py
 
const EPOCH = 1721425.5;
 
const INTERCALATION_CYCLE_YEARS = 400;
const INTERCALATION_CYCLE_DAYS = 146097;
 
const LEAP_SUPPRESSION_YEARS = 100;
const LEAP_SUPPRESSION_DAYS = 36524;
 
const LEAP_CYCLE_YEARS = 4;
const LEAP_CYCLE_DAYS = 1461;
 
const YEAR_DAYS = 365;
 
const HAVE_30_DAYS = [4, 6, 9, 11];
 
export class GregorianDate extends HistoricalDate {
    public get isLeapYear() {
        return this.year !== undefined && isLeapYear(this.year, this.calendar);
    }
 
    constructor(public readonly year: number | undefined,
        public readonly month: number | undefined,
        public readonly day: number | undefined) {
        super('gregorian');
        this.assertLegalDate(year, month, day);
    }
 
    static fromJulianDays(jd: JulianDays) {
        const days = jd.days;
        let wjd = Math.floor(days - 0.5) + 0.5;
        let depoch = wjd - EPOCH;
 
        let quadricent = Math.floor(depoch / INTERCALATION_CYCLE_DAYS);
        let dqc = depoch % INTERCALATION_CYCLE_DAYS;
 
        let cent = Math.floor(dqc / LEAP_SUPPRESSION_DAYS);
        let dcent = dqc % LEAP_SUPPRESSION_DAYS;
 
        let quad = Math.floor(dcent / LEAP_CYCLE_DAYS);
        let dquad = dcent % LEAP_CYCLE_DAYS;
 
        let yindex = Math.floor(dquad / YEAR_DAYS);
        let year = quadricent * INTERCALATION_CYCLE_YEARS +
            cent * LEAP_SUPPRESSION_YEARS +
            quad * LEAP_CYCLE_YEARS + yindex;
 
        if (cent != 4 && yindex != 4) {
            year += 1;
        }
 
        let yearDay = wjd - GregorianDate.toJulianDays(year, 1, 1);
 
        let leap = isLeapYear(year);
        let leapAdj: number;
        if (yearDay < (58 + (leap ? 1 : 0))) {
            leapAdj = 0;
        } else if (leap) {
            leapAdj = 1;
        } else {
            leapAdj = 2;
        }
 
        let month = Math.floor((((yearDay + leapAdj) * 12) + 373) / 367);
        let day = Math.trunc(wjd - GregorianDate.toJulianDays(year, month, 1)) + 1;
 
        return new GregorianDate(
            jd.unknownYear ? undefined : year,
            jd.unknownMonth ? undefined : month,
            jd.unknownDay ? undefined : day);
    }
 
    static fromString(text: string) {
        const parsed = parseDateString(text);
        if (parsed) {
            return new GregorianDate(parsed.year, parsed.month, parsed.day);
        }
        throw new InvalidDateException();
    }
 
    public addDays(days: number) {
        if (days == 0) { return this; }
        const jd = this.toJulianDays()
        jd.days += days;
        return GregorianDate.fromJulianDays(jd);
    }
 
    public toGregorian() {
        return this;
    }
 
    public toJulian() {
        return JulianDate.fromJulianDays(this.toJulianDays());
    }
 
    public toString() {
        return `${this.year === undefined ? '??' : this.year}-${this.month === undefined ? '??' : this.month}-${this.day === undefined ? '??' : this.day}`;
    }
 
    /**
     * Gregorian to Julian Day Count for years between 1801-2099
     * @see http://quasar.as.utexas.edu/BillInfo/JulianDatesG.html
     */
    private static toJulianDays1801(year: number, month: number, day: number) {
        if (month <= 2) {
            year = year - 1;
            month = month + 12;
        }
 
        let a = Math.floor(year / 100);
        let b = Math.floor(a / 4);
        let c = 2 - a + b;
        let e = Math.floor(365.25 * (year + 4716));
        let f = Math.floor(30.6001 * (month + 1));
 
        return c + day + e + f - 1524.5
    }
 
    private static toJulianDays(year: number, month: number, day: number) {
        Eif (year >= 1801 || year <= 2099) {
            return this.toJulianDays1801(year, month, day);
        }
 
        let leapAdj;
 
        if (month <= 2) {
            leapAdj = 0
        } else if (isLeapYear(year)) {
            leapAdj = -1
        } else {
            leapAdj = -2
        }
 
        return EPOCH - 1 + (YEAR_DAYS * (year - 1)) +
            Math.floor((year - 1) / LEAP_CYCLE_YEARS) +
            (-Math.floor((year - 1) / LEAP_SUPPRESSION_YEARS)) +
            Math.floor((year - 1) / INTERCALATION_CYCLE_YEARS) +
            Math.floor((((367 * month) - 362) / 12) + leapAdj + day);
    }
 
    private toJulianDays(): JulianDays {
        const days = GregorianDate.toJulianDays(this.year || 1, this.month || 1, this.day || 1);
        return {
            days,
            unknownYear: this.year === undefined,
            unknownMonth: this.month === undefined,
            unknownDay: this.day === undefined
        }
    }
 
    /**
     * Check if this is a legal date in the Gregorian calendar
     */
    private assertLegalDate(year: number | undefined, month: number | undefined, day: number | undefined) {
        let daysInMonth: number;
        if (month == 2) {
            daysInMonth = isLeapYear(year) ? 29 : 28;
        } else {
            daysInMonth = HAVE_30_DAYS.indexOf(month || 1) >= 0 ? 30 : 31;
        }
 
        Iif (day !== undefined && (day < 0 || day > daysInMonth)) {
            throw new InvalidDateException(`Month ${month} doesn't have a day ${day}`);
        }
 
        return true;
    }
}